import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import java.util.Arrays;
import java.util.NoSuchElementException;
import java.util.Scanner;
import java.util.Vector;

/**
 * Front end UI for dvd-system
 * 
 * This is the main program for the dvd system and must be invoked with two
 * arguments: the input dvd file the output transaction file
 * 
 * When the user performs the logout command, the DVDSystem will terminate.
 * 
 * @author Adam Contois, Adam Domurad, Pat Goebel
 * 
 */
public class DVDSystem {
	// maximums for various constraints on the system
	private final static int maxCopies = 999;
	private final static int maxRentCopies = 3;
	private final static int maxBuyCopies = 5;
	private final static int maxNameLength = 25;
	// One minute timeout
	private final static DVDTimer timer = new DVDTimer(60 * 1000);
	// Commands for standard usage of system
	private final static List<String> cmdsStandard = Arrays
			.asList(new String[] { "rent", "return", "buy", "logout" });
	// All commands, in order of transaction code
	private final static List<String> cmdsAdmin = Arrays.asList(new String[] {
			"logout", "rent", "return", "create", "add", "remove", "buy",
			"sell" });

	/**
	 * Return transaction code for a given command
	 * 
	 * @param command
	 *            string representing command
	 * @return -1 if invalid command, transaction code otherwise
	 */
	private static int getCommandCode(String command) {
		return cmdsAdmin.indexOf(command);
	}

	/**
	 * Perform a single user session (login until logout)
	 * 
	 * @param db
	 *            the database to work with
	 */
	private static boolean performSession(Scanner scan, DVDDatabase db) {
		int seshtype = -1;
		String line = "", command;
		while (true) {
			command = scan.next();
			if (command.equals("exit")) {
				return false;
			} else if (command.equals("login")) {
				while (true) {
					timer.resetTimer();
					System.out.println("Specify session type:");
					// get the session type
					line = scan.next();
					// if login successful, update seshtype and exit loop else
					// remain in loop
					if (line.equals("standard")) {
						seshtype = 0;
						break;
					} else if (line.equals("admin")) {
						seshtype = 1;
						break;
					} else {
						System.out.println("No such session type.");
					}
				}
				System.out.println("You are now logged in.");
				break;
			} else {
				if (command.equals("logout"))
					System.out.println("You are not logged in!");
				else
					System.out
							.println("You cannot perform this action without being logged in.");
			}
		}
		// Next input will be a command for standard or admin user
		while (true) {
			timer.resetTimer();
			line = scan.next();
			if (line.equals("login")) {
				System.out.println("You are already logged in!");
				continue;
			}
			if (line.equals("logout")) {
				db.addTransaction(new DVDTransaction(getCommandCode("logout"),
						"", 0, DVDTransaction.STATUS_NA, 0));
				System.out.println("You are now logged out.");
				break;
			}
			if (seshtype == 1) {
				if (cmdsAdmin.contains(line)) {
					performCommand(scan, line, db);
				} else
					System.out.println("Expected valid admin user action.");
			} else if (seshtype == 0) {
				if (cmdsStandard.contains(line)) {
					performCommand(scan, line, db);
				} else
					System.out.println("Expected valid standard user action.");
			}
		}
		return true;
	}

	/**
	 * The main program, usage <input dvd-file> <output transaction-file>
	 * 
	 * @param args
	 *            The command-line arguments, [dvd-file, transaction-file]
	 */
	public static void main(String[] args) {
		timer.start();
		DVDDatabase database;
		if (args.length != 2) {
			System.out.println("Expected usage: java DVDSystem <input dvd-file> <output transaction-file>");
			System.exit(0);
		}
		String dvdfile = args[0], transactionfile = args[1];
		try {
			database = DVDDatabase.createDVDDatabase(dvdfile);
			if (database == null) {
				// Create empty transaction file:
				new FileWriter(transactionfile).close();
				System.exit(0);
			}
		} catch (IOException e) {
			System.out.println("Problem loading DVD file.");
			System.exit(0);
            return;
		}
		Scanner scan = new Scanner(System.in);
		System.out.println("Welcome to the DVD rental system!");
		try {
			while (true) {
				timer.resetTimer();
				if (!performSession(scan, database)){
					break;
				}
			}
		} catch (NoSuchElementException nsee){
		}
		try {
			database.toDVDTransactionFile(transactionfile);
		} catch (IOException e) {
			System.out.println("Problem writing to transaction file.");
		}
		System.exit(0);
	}

	/**
	 * Processes user DVD
	 * 
	 * @param e
	 *            entry to validate query for
	 * @param scan
	 *            stream from which to take input
	 * @param db
	 *            the database to work with
	 */
	private static int askForDVDAmount(Scanner scan, DVDDatabase db) {
		System.out.println("Specify DVD amount:");
		int copies = Integer.parseInt(scan.next());

		if (copies <= 0)
			throw new InvalidUserInputException(
					"Invalid number of copies (less than or equal to 0).");

		return copies;
	}

	/**
	 * Processes input price
	 * 
	 * @param e
	 *            entry to validate query for
	 * @param scan
	 *            stream from which to take input
	 * @param db
	 *            the database to work with
	 * @return the input price in cents
	 */
	private static int askForDVDPrice(Scanner scan, DVDDatabase db) {
		System.out.println("Specify DVD price:");
		int priceInCents = (int) (Double.parseDouble(scan.next()) * 100);

		if (priceInCents <= 0)
			throw new InvalidUserInputException(
					"Invalid price, negative amounts not allowed.");

		if (priceInCents > 150 * 100)
			throw new InvalidUserInputException(
					"Sale price of a particular DVD can not exceed $150.00");
		return priceInCents;
	}

	/**
	 * Processes user DVD name entries
	 * 
	 * @param scan
	 *            stream from which to take input
	 * @param db
	 *            the database to work with
	 */
	private static String askForDVDName(Scanner scan, DVDDatabase db) {
		DVDEntry entry;
		System.out.println("Specify DVD name:");
		String name = scan.nextLine();
		if (name.length() > maxNameLength) {
			throw new InvalidUserInputException(
					"The DVD name provided is too long, please limit your name to 25 characters.");
		}
		for (char c : name.toCharArray()) {
			if (!Character.isLetterOrDigit(c) && !Character.isWhitespace(c)
					&& c != '_') {
				throw new InvalidUserInputException(
						"Unexpected character in DVD name.");
			}
		}
		return name;
	}

	/**
	 * Process user commands related to renting a for-rent DVD. The database
	 * passed is updated, and a transaction is created.
	 * 
	 * @param scan
	 *            stream from which to take input
	 * @param db
	 *            the database to work with
	 */
	private static void processRent(Scanner scan, DVDDatabase db) {
		// Throws exception if entry does not exist
		DVDEntry entry = db.getDVDEntry(askForDVDName(scan, db));

		if (!entry.getRentalStatus())
			throw new InvalidDVDException(
					"DVD can not be rented, it has been listed as for sale.");

		int copies = askForDVDAmount(scan, db);

		if (copies > maxRentCopies)
			throw new InvalidUserInputException(
					"You cannot rent more than 3 copies of a DVD.");

		if (copies > entry.getAmount())
			throw new InvalidDVDException("Insufficient copies of '"
					+ entry.getName() + "' remaining.");
		if (copies <= 0)
			throw new InvalidUserInputException(
					"Invalid number of copies (less than or equal to 0).");

		int priceCents = copies * entry.getPriceInCents();
		entry.setAmount(entry.getAmount() - copies);
		System.out.println(copies + " copy of '" + entry.getName()
				+ "' has been rented successfully.");
		// Add rental transaction, dynamically evaluating the transaction code
		db.addTransaction(new DVDTransaction(getCommandCode("rent"), entry
				.getName(), copies, DVDTransaction.STATUS_RENTAL, priceCents));
	}

	/**
	 * Process user commands related to returning a rented DVD. The database
	 * passed is updated, and a transaction is created.
	 * 
	 * @param scan
	 *            stream from which to take input
	 * @param db
	 *            the database to work with
	 */
	private static void processReturn(Scanner scan, DVDDatabase db) {
		DVDEntry entry;
		int transCode, copies, priceCents;
		String name;
		boolean rental;

		entry = db.getDVDEntry(askForDVDName(scan, db));

		if (!entry.getRentalStatus()) {
			throw new InvalidDVDException(
					"DVD can not be returned, it has been listed as for sale.");
		}
		copies = askForDVDAmount(scan, db);

		if (copies > maxRentCopies)
			throw new InvalidUserInputException(
					"You cannot return more than 3 copies of a DVD.");

		entry.setAmount(entry.getAmount() + copies);
		System.out.println(copies + " copy of '" + entry.getName()
				+ "' has been returned successfully.");
		// Add rental transaction, dynamically evaluating the transaction code
		db.addTransaction(new DVDTransaction(getCommandCode("return"), entry
				.getName(), copies, DVDTransaction.STATUS_RENTAL, 0));
	}

	/**
	 * Process user commands related to creating a a new DVD entry. The session
	 * must be an admin session. The database passed is updated, and a
	 * transaction is created.
	 * 
	 * @param scan
	 *            stream from which to take input
	 * @param db
	 *            the database to work with
	 */
	private static void processCreate(Scanner scan, DVDDatabase db) {
		String name = askForDVDName(scan, db);

		if (db.hasDVDEntry(name))
			throw new InvalidUserInputException(
					"A DVD already exists with the title '" + name + "'.");

		int copies = askForDVDAmount(scan, db);

		if (copies > maxCopies)
			throw new InvalidUserInputException(
					"Number of copies of DVD exceeds maximum of 999.");

		int priceCents = askForDVDPrice(scan, db);

		db.addDVDEntry(new DVDEntry(name, copies, priceCents, true, true));

		System.out.print("You have created the DVD '" + name + "'");
		System.out.println(", listed for rental at "
				+ FormatUtil.formatPrice(priceCents) + ", with " + copies
				+ " copy.");
		// Add rental transaction, dynamically evaluating the transaction code
		db.addTransaction(new DVDTransaction(getCommandCode("create"), name,
				copies, DVDTransaction.STATUS_RENTAL, priceCents));
	}

	/**
	 * Process user commands related to adding copies of an existing DVD. The
	 * session must be an admin session. The database passed is updated, and a
	 * transaction is created.
	 * 
	 * @param scan
	 *            stream from which to take input
	 * @param db
	 *            the database to work with
	 */
	private static void processAdd(Scanner scan, DVDDatabase db) {
		DVDEntry entry;
		int copies;
		String name;

		System.out.println("Specify DVD name:");
		name = scan.nextLine();
		// Throws invalid DVD Exception
		entry = db.getDVDEntry(name);
		copies = askForDVDAmount(scan, db);

		if (copies + entry.getAmount() > maxCopies) {
			throw new InvalidUserInputException(
					"Number of copies of DVD exceeds maximum of 999.");
		}

		entry.setAmount(entry.getAmount() + copies);
		System.out.println(copies + " copy of '" + name
				+ "' has been added successfully.");

		// Add rental transaction, dynamically evaluating the transaction code
		db.addTransaction(new DVDTransaction(getCommandCode("add"), name,
				copies, DVDTransaction.STATUS_RENTAL, 0));
	}

	/**
	 * Process user commands related to removing an existing DVD entry. The
	 * session must be an admin session. The database passed is updated, and a
	 * transaction is created.
	 * 
	 * @param scan
	 *            stream from which to take input
	 * @param db
	 *            the database to work with
	 */
	private static void processRemove(Scanner scan, DVDDatabase db) {
		String name = askForDVDName(scan, db);

		if (!db.hasDVDEntry(name))
			throw new InvalidDVDException("The DVD '" + name
					+ "' is not available.");

		db.remove(name);
		System.out.println("'" + name + "' successfully removed.");

		// Add rental transaction, dynamically evaluating the transaction code
		db.addTransaction(new DVDTransaction(getCommandCode("remove"), name, 0,
				DVDTransaction.STATUS_NA, 0));
	}

	/**
	 * Process user commands related to buying a for-sale DVD entry. The
	 * database passed is updated, and a transaction is created.
	 * 
	 * @param scan
	 *            stream from which to take input
	 * @param db
	 *            the database to work with
	 */
	private static void processBuy(Scanner scan, DVDDatabase db) {
		DVDEntry entry = db.getDVDEntry(askForDVDName(scan, db));

		if (entry.getRentalStatus())
			throw new InvalidDVDException("The DVD '" + entry.getName()
					+ "' is not available for sale");

		int copies = askForDVDAmount(scan, db);

		if (copies > maxBuyCopies)
			throw new InvalidUserInputException(
					"Sorry, you may only purchase up to five copies of a DVD title per transaction");
		if (copies > entry.getAmount())
			throw new InvalidDVDException("Insufficient copies of '"
					+ entry.getName() + "' remaining.");

		int priceCents = copies * entry.getPriceInCents();
		entry.setAmount(entry.getAmount() - copies);
		System.out.println(copies + " copy of '" + entry.getName()
				+ "' has been purchased successfully.");

		// Add rental transaction, dynamically evaluating the transaction code
		db.addTransaction(new DVDTransaction(getCommandCode("buy"), entry
				.getName(), copies, DVDTransaction.STATUS_SALE, priceCents));
	}

	private static void processSell(Scanner scan, DVDDatabase db) {
		DVDEntry entry;
		int priceCents;
		String name;

		entry = db.getDVDEntry(askForDVDName(scan, db));
		priceCents = askForDVDPrice(scan, db);
		entry.setPriceInCents(priceCents);
		entry.setRentalStatus(false);
		System.out.println("DVD '" + entry.getName()
				+ "' successfully listed for sale for "
				+ FormatUtil.formatPrice(priceCents) + ".");

		// Add rental transaction, dynamically evaluating the transaction code
		db.addTransaction(new DVDTransaction(getCommandCode("sell"), entry
				.getName(), entry.getAmount(), DVDTransaction.STATUS_SALE,
				priceCents));
	}

	/**
	 * Performs a command, using user input
	 * 
	 * @param scan
	 *            the scanner passed from main
	 * @param line
	 *            the command line
	 * @param db
	 *            the DVD database
	 */
	private static void performCommand(Scanner scan, String line, DVDDatabase db) {
		// This is needed to advance input past the newline left by calls to
		// next()
		scan.nextLine();
		int transCode = getCommandCode(line);
		timer.resetTimer();
		try {
			switch (transCode) {
			case 01:// "rent"
				processRent(scan, db);
				break;
			case 02:// "return"
				processReturn(scan, db);
				break;
			case 03:// "create"
				processCreate(scan, db);
				break;
			case 04:// "add"
				processAdd(scan, db);
				break;
			case 05:// "remove"
				processRemove(scan, db);
				break;
			case 06:// "buy"
				processBuy(scan, db);
				break;
			case 07:// "sell"
				processSell(scan, db);
				break;
			default:
				System.out.println("Expected valid user action.");
				return;
			}
		} catch (InvalidUserInputException iuie) {
			System.out.println(iuie.getMessage());
		} catch (InvalidDVDException dvde) {
			System.out.println(dvde.getMessage());
		} catch (NumberFormatException dvde) {
			System.out.println("Unexpected characters in input, expecting number.");
		}
	}
}